<?php
/* $Id: class.tinytemplater.php 4 2009-06-14 04:48:46Z intel352 $ */
/**
 * TinyTemplater
 *
 * Light templating class,
 * adds support for LOGIC statements
 * and variables within your content.
 *
 * @author Jon Langevin aka intel352 - jon@phpsitesolutions.com
 * @link http://code.google.com/p/tinytemplater/
 * @copyright GNU LGPL
 * @since 5/27/2009
 * @version 1.0.0
 *
 */
class TinyTemplater {
	private $_content = '';
	private $_globals = array();
	private $_open_brace = '[';
	private $_close_brace = ']';

	private $_regex_prepped = false;
	private $_outer_conditional_regex = '';
	private $_inner_conditional_regex = '';
	private $_logical_operators = array('and', 'or', 'xor', '&&', '||');
	private $_logical_operators_regex = '';
	private $_comparison_operators = array('===', '!==', '==', '!=', '<=', '>=', '~=', '=', '<>', '<', '>');
	private $_comparison_operators_regex = '';
	private $_variable_regex = '';

	public function __construct() {
		#;
	}

	/**
	 * Parse a string
	 *
	 * @param string $string
	 * @return boolean
	 */
	public function parseString($string) {
		if(empty($string)) return false;
		$this->_content = $string;

		$this->_matchConditionals();
		$this->_matchVariables();

		return true;
	}

	/**
	 * Parse a template file
	 *
	 * @param string $filename
	 * @return boolean
	 */
	public function parseFile($filename) {
		if(file_exists($filename)) {
			return $this->parseString(file_get_contents($filename));
		}

		return false;
	}

	/**
	 * Render the content to the page
	 *
	 * @param boolean $return Whether to return result as string (default false)
	 * @return string
	 */
	public function render($return = false) {
		if(!empty($return)) {
			return $this->_content;
		}else{
			print $this->_content;
		}
		unset($this->_content);
	}

	/**
	 * Assign variables for use within text
	 *
	 * @param string $name
	 * @param mixed $value
	 */
	public function assignVariable($name, $value) {
		$this->_globals[$name] =& $value;
	}

	/**
	 * Match all variables
	 *
	 * @param string $string Optional string argument
	 * @return string
	 */
	private function _matchVariables($string = false) {
		$this->_prepareRegex();

		if($string===false) {
			$string =& $this->_content;
		}

		if(empty($string)) {
			return '';
		}

		$string = preg_replace_callback($this->_variable_regex, array($this, '_replaceVariables'), $string);

		return $string;
	}

	/**
	 * Replace all detected variables
	 *
	 * @param array $variable
	 * @return string
	 */
	private function _replaceVariables($variable) {
		$this->_prepareRegex();

		if(empty($variable[1])) return '';

		$variable_pieces = explode('.', $variable[1]);

		$action = $params = '';
		if(!empty($variable[2]) && preg_match('/^(\w+)\=([\'"])(.*)\2$/i', $variable[2], $matches) && !empty($matches[1]) && !empty($matches[3])) {
			$action = $matches[1];
			$params = $matches[3];
		}

		if(!empty($this->_globals)) {
			$i=0;
			$num_pieces = count($variable_pieces);
			$_tmpvar =& $this->_globals;
			foreach($variable_pieces AS $k=>$v) {
				$i++;
				if(is_array($_tmpvar)) {
					if(array_key_exists($v, $_tmpvar)) {
						$_tmpvar =& $_tmpvar[$v];
					}else{
						return '';
					}
				}elseif(is_object($_tmpvar)) {
					if(($_objvars = get_object_vars($_tmpvar)) && array_key_exists($v, $_objvars)) {
						$_tmpvar =& $_tmpvar->$v;
						unset($_objvars);
					}else{
						return '';
					}
				}else{
					return '';
				}

				if($i==$num_pieces) {
					if(!is_array($_tmpvar) && !is_object($_tmpvar)) {
						if(is_bool($_tmpvar) && ($_tmpvar === true || $_tmpvar === false)) {
							$_tmpvar = $_tmpvar ? 'true' : 'false';
						}else{
							$_tmpvar = (string) $_tmpvar;
						}
						if(!empty($action) && !empty($params)) {
							switch($action) {
								case 'match':
									if(preg_match($params, $_tmpvar, $matches)) {
										$_tmpvar = $matches[0];
									}
								break;
							}
						}
						return $_tmpvar;
					}else{
						return '';
					}
				}
			}
		}

		return '';
	}

	/**
	 * Match IF/ELSE statements, and evaluate result
	 *
	 * @param string $string Optional string to check
	 * @return string
	 */
	private function _matchConditionals($string = false) {
		$this->_prepareRegex();

		$return_string = false;
		$conditions = array('if'=>array(), 'else'=>'');
		$outer_condition = '';
		if($string===false) {
			$string =& $this->_content;
		}elseif(is_array($string)) {
			$return_string = true;
			$outer_condition = $string[1];
			$string = $string[2];
		}

		if(empty($string)) {
			return '';
		}

		$string = preg_replace_callback($this->_outer_conditional_regex, array($this, '_matchConditionals'), $string);

		if(!empty($return_string)) {
			$matched = preg_match_all($this->_inner_conditional_regex, $string, $matches, PREG_SET_ORDER);
			if(!empty($matched)) {
				foreach($matches AS $match) {
					if(empty($match[1]) && empty($match[2]) && empty($match[3])) continue;

					if(!empty($match[1])) {
						$conditions['if'][$outer_condition] = trim($match[1]);
					}
					if(!empty($match[2])) {
						$conditions['if'][$match[2]] = trim($match[3]);
					}else{
						$conditions['else'] = trim($match[3]);
					}
				}

				return $this->_evalConditionals($conditions);
			}
		}
	}

	/**
	 * Evaluate IF conditions
	 *
	 * @param array $conditions Array of IF/ELSE
	 * @return string
	 */
	private function _evalConditionals($conditions = array()) {

		if(empty($conditions) || (empty($conditions['if']) && !isset($conditions['else']))) {
			return '';
		}elseif(empty($conditions['if']) && isset($conditions['else'])) {
			return $conditions['else'];
		}

		foreach($conditions['if'] AS $expr=>$val) {
			$to_eval = array_map('trim', preg_split($this->_logical_operators_regex, $expr, -1, PREG_SPLIT_DELIM_CAPTURE) );

			$return = array();
			foreach($to_eval AS $comparison) {
				# Need to record the logical operator
				if(in_array($comparison, $this->_logical_operators)) {
					$return[] = $comparison;
					continue;
				}

				list($left_arg, $operator, $right_arg) = array_map('trim', preg_split($this->_comparison_operators_regex, $comparison, -1, PREG_SPLIT_DELIM_CAPTURE) );

				if(empty($left_arg) && empty($operator) && empty($right_arg)) {
					continue;
				}elseif(!empty($right_arg) && (empty($operator) || !in_array($operator, $this->_comparison_operators))) {
					continue;
				}

				/**
				 * Find quoted string, remove quotes
				 */
				if(preg_match('/^([\'"])(.*)\1$/s', $left_arg, $matches)) {
					$left_arg = $matches[2];
				}
				if(preg_match('/^([\'"])(.*)\1$/s', $right_arg, $matches)) {
					$right_arg = $matches[2];
				}

				/**
				 * Replace variables within arguments
				 */
				$left_arg = $this->_matchVariables($left_arg);
				$right_arg = $this->_matchVariables($right_arg);

				/**
				 * Determine the result from the comparison
				 */
				$result = false;
				switch($operator) {
					case '~=':
						if(($_tmp = preg_match($right_arg, $left_arg)) && !empty($_tmp)) {
							$result = true;
							unset($_tmp);
						}
					break;
					case '=':
					case '==':
						if($left_arg == $right_arg) {
							$result = true;
						}
					break;
					case '===':
						if($left_arg === $right_arg) {
							$result = true;
						}
					break;
					case '!=':
						if($left_arg != $right_arg) {
							$result = true;
						}
					break;
					case '<>':
						if($left_arg <> $right_arg) {
							$result = true;
						}
					break;
					case '!==':
						if($left_arg !== $right_arg) {
							$result = true;
						}
					break;
					case '<':
						if($left_arg < $right_arg) {
							$result = true;
						}
					break;
					case '>':
						if($left_arg > $right_arg) {
							$result = true;
						}
					break;
					case '<=':
						if($left_arg <= $right_arg) {
							$result = true;
						}
					break;
					case '>=':
						if($left_arg >= $right_arg) {
							$result = true;
						}
					break;
				}
				unset($left_arg, $operator, $right_arg);

				$return[] = $result ? 'true' : 'false';

				/**
				 * If the result is false, no need to check the other conditions
				 */
				if($result == false) {
					break;
				}
			}

			$eval_string = implode(' ', $return);
			$eval_string = 'return (' . (empty($eval_string) ? 'false' : $eval_string) . ');';
			if(eval($eval_string)) {
				return $val;
			}
		}

		/**
		 * If nothing has eval'd true, return ELSE or empty string
		 */
		if(isset($conditions['else'])) {
			return $conditions['else'];
		}else{
			return '';
		}
	}

	/**
	 * Prepare all class regex for use
	 */
	private function _prepareRegex() {
		if(empty($this->_regex_prepped)) {
			$this->_open_brace = preg_quote($this->_open_brace, '/');
			$this->_close_brace = preg_quote($this->_close_brace, '/');

			$_tmp = array();
			foreach($this->_logical_operators AS $v) {
				$_tmp[] = preg_quote($v, '/');
			}
			$this->_logical_operators_regex = '/(' . implode('|', $_tmp) . ')/i';

			$_tmp = array();
			foreach($this->_comparison_operators AS $v) {
				$_tmp[] = preg_quote($v, '/');
			}
			$this->_comparison_operators_regex = '/(' . implode('|', $_tmp) . ')/i';

			$ob =& $this->_open_brace;
			$cb =& $this->_close_brace;

			/**
			 * Used to match OUTER IF statement
			 */
			$this->_outer_conditional_regex = '/' . $ob . 'if\s+((?:' . $ob . '(?:(?>[^' . $ob . $cb . ']+)|(?1))' . $cb . '|[^' . $ob . $cb . ']*?)+)' . $cb . '((?>' . $ob . '(?!(?:end\s*)?if(?:\s+((?:' . $ob . '(?:(?>[^' . $ob . $cb . ']+)|(?3))' . $cb . '|[^' . $ob . $cb . ']*?)+))?' . $cb . ')|[^' . $ob . ']|(?R))*)' . $ob . 'end\s*if' . $cb . '(?:\<br\s*\/\>|\n)?/is';

			/**
			 * Matches internal content of OUTER IF
			 */
			$this->_inner_conditional_regex = '/(?:^((?:' . $ob . '(?!else(?:\s*if\s+[^' . $ob . $cb . ']+)?)(?:(?>[^' . $ob . $cb . ']+)|(?1))' . $cb . '|[^' . $ob . $cb . ']*)+)*)?(?:' . $ob . 'else(?:\s*if\s+((?:' . $ob . '(?!else(?:\s*if\s+[^' . $ob . $cb . ']+)?)(?:(?>[^' . $ob . $cb . ']+)|(?2))' . $cb . '|[^' . $ob . $cb . ']*?)+))?' . $cb . '((?:(?!' . $ob . 'else.*?' . $cb . ').)+))?/si';

			/**
			 * Matches variables
			 */
			$this->_variable_regex = '/' . $ob . '(\w[.\w]*)(?:;\s*(.*?)?)?' . $cb . '/i';

			$this->_regex_prepped = true;
		}
	}
}

?>